package collaboration.client;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Calendar;

import ocsf.client.ObservableClient;

import collaboration.common.CollabIF;
import collaboration.common.CollaborationApp;
import collaboration.common.apps.*;

import collaboration.server.CollabServer;
import collaboration.ui.CollabClientUI;

/**
 * The <code>CollabClient</code> contains all the methods necessary to set
 * up the client and server side of a client-server-collaborate architecture.
 * 
 * This class contains the <code>main</code> method to instantiate the class and 
 * start the GUI.
 * 
 * <code>CollabClient</code> requires a loginID as the initial argument and prints an
 * error message otherwise.
 * 
 * @author Trevor Clack 6286191
 * @author Bowen Cheng 6204467
 * @version November 26, 2012 (1.36)
 *
 */
public class CollabClient extends ObservableClient{
	//Class variables **********************************************
	/**
	 * The newline character to use.
	 */
	final public static String NEWLINE = System.getProperty("line.separator");

	/**
	 * The name of the program.
	 */
	final public static String PROGRAM_NAME = "Collaboration Client";

	/**
	 * The version number of the program.
	 */
	final public static String PROGRAM_VERSION = "1.36";
	
	/**
	 * The valid Collaborative Applications that <code>CollabClient</code> can run.
	 */
	//NOTE:If adding applications append them at the end of the array!!
	final public static String validApps[][] = {
		{"TicTacToe", Long.toString(TicTacToe.serialVersionUID)}
		};

	/**
	 * The Commands and their descriptions.
	 */
	//NOTE:If adding new commands append them at the end of the array!!
	final public static String[][] COMMANDS = { 
		{"#quit","Disconnects from the server and goes back to the main screen for " + PROGRAM_NAME + "."},
		{"#logoff","Disconnects from the server but keeps " +  PROGRAM_NAME + " running."},
		{"#sethost","Changes the host to connect to." +
			" You must be logged off to run \"#sethost\"!" + 
			" Usage: #sethost <host>"},
		{"#setport","Changes the port to connect on." + 
			" You must be logged off to run \"#setport\"!" + 
			" Usage: #setport <port number>"},
		{"#login","Connects to the server." + 
			" You must be logged off to run \"#login\"!" +
			" Usage: #login or #login <LoginID>"},
		{"#gethost","Displays the current hostname."},
		{"#getport","Displays the current port number."},
		{"#debug", "Turn debug on or off. Toggles between on and off."}
	};

	/**
	 * The default host.
	 */
	final public static String DEFAULT_HOST = "localhost";

	/**
	 * The default port.
	 */
	final public static int DEFAULT_PORT = 5700;

	//------------------------
	// MEMBER VARIABLES
	//------------------------

	/**
	 * The loginID variable.
	 */
	private String loginID;
	
	/**
	 * A Calendar instance that returns current real time
	 */
	private	Calendar aCalender;
	
	/**
	 * The last message received from the server
	 */
	private String lastMsg;
	
	/**
	 * The second to last message received from the server
	 */
	private String secondLastMsg;

	/**
	 * The boolean flag for whether debug function is turned on.
	 */	
	private boolean debug;

	//collabClient Associations
	private CollabIF collabClientUI;
	private CollabServer collabServer;
//	private List<CollaborationApp> collaborationApps;
	private CollaborationApp collaborationApps[];

	//------------------------
	// CONSTRUCTOR
	//------------------------

	/**
	 * The <code>CollabClient</code> constructor.
	 * 
	 * @param loginID The unique loginID for this client
	 * @throws IOException The error thrown.
	 */
	public CollabClient(String loginID) throws IOException {
		super(DEFAULT_HOST, DEFAULT_PORT);
		debug=false;//debug is off by default
		if (loginID!=null && !loginID.equals("")){
			String[] loginArray = loginID.split(" ");
			if(loginArray.length > 1){//No Spaces allowed
				throw new IOException("Error: Spaces are not allowed in the loginID");
			}else{
				this.loginID = loginID;
				aCalender = Calendar.getInstance();
				lastMsg = "";
				secondLastMsg = "";
				collaborationApps = new CollaborationApp[maximumNumberOfCollaborationApps()];
				for(int index = 0; index < maximumNumberOfCollaborationApps(); index++){
					collaborationApps[index] = new CollaborationApp(this,index);
				}
				if(!setCollabClientUI(new CollabClientUI(this))){
					throw new IOException("Error: Unable to start " + PROGRAM_NAME + "!");
				}
				collabClientUI.display("Hello " + loginID + "!" + NEWLINE + "Welcome to " + PROGRAM_NAME + ".");
			}
		}else{
			throw new IOException("Error: loginID is missing.");
		}
	}
	
	/**
	 * Returns the current <code>debug</code> setting.
	 * 
	 * @return The current <code>debug</code> setting.
	 */
	public boolean getDebug(){
		return debug;
	}
	
	/**
	 * Returns the current <code>loginID</code> setting.
	 * 
	 * @return The current <code>loginID</code>.
	 */
	public String getLoginID(){
		return loginID;
	}

	/**
	 * This method handles all data that comes in from the server.
	 *
	 * @param msg The message from the server.
	 */
	@Override
	public void handleMessageFromServer(Object msg){
		setChanged();
		notifyObservers();
		collabClientUI.display(msg);
		addRecord(msg);
	}

	/**
	 * This method handles all data that goes to the log file.
	 *
	 * @param msg The message to append in the log file.
	 */
	public void addRecord(Object msg) {
		String tempLog = msg.toString();
		if(!lastMsg.equals(tempLog) && !secondLastMsg.equals(tempLog)){//Reduce duplicates in the log
			try{
				FileWriter fstream = new FileWriter(loginID + "ChattingLog.txt",true);
				BufferedWriter fbw = new BufferedWriter(fstream);
				fbw.write(aCalender.getTime()+ " -> " + tempLog);
				fbw.newLine();
				fbw.close();
				secondLastMsg = lastMsg;
				lastMsg = tempLog;
			}
			catch (Exception e)
			{
				collabClientUI.display("Warning: cant open chatting log file!");
				if(debug)
					collabClientUI.display("Error: " + e.getMessage());
			}
		}
	}

	/**
	 * This method handles connecting and passing the loginID to the server.
	 * 
	 *@throws IOException The error thrown by either <code>openConnection</code> or <code>sendToServer</code>.
	 */
	protected void handleConnectionToServer() throws IOException{
		openConnection();
		sendToServer("#login " + loginID);
	}

	/**
	 * The <code>main</code> method that creates an instance of <code>CollabClient</code>.
	 * It prints a message to <code>System.err</code> for any problems.
	 *
	 *@param args The loginID as <code>arg[0]</code>.
	 */
	public static void main(String[] args) {
		@SuppressWarnings("unused")
		CollabClient client;
		if(args.length > 1){
			System.err.println("Error: Unable to start " + PROGRAM_NAME +
					". Too many arguments!" + NEWLINE +
					" \tUsage: \" CollabClient <loginID>\", where <loginID> is a unique identifier for your connection." +
					NEWLINE + " \tNote: No spaces are allowed in <loginID>.");
		}else{
			try {
				client = new CollabClient(args[0]);//args[0] is the LoginID
			} catch (IOException e) {
				System.err.println("Error: Unable to start " + PROGRAM_NAME + "." + NEWLINE + e.toString());
			} catch(ArrayIndexOutOfBoundsException e){
				System.err.println("Error: loginID is missing." + NEWLINE +
						" \tUsage: \" CollabClient <loginID>\", where <loginID> is a unique identifier for your connection." +
						NEWLINE + " \tNote: No spaces are allowed in <loginID>.");
			}
		}
	}
	
	/**
	 * This method handles all data that comes in from the UI.
	 * 
	 * @param message The message from the UI.
	 */
	public void handleMessageFromClientUI(String message)
	{

		if (message.startsWith("#")) { // All commands should start with #
			String[] clientString =  message.split(" ");// Get the command and any other parameters
			int clientCommand = -1; // Initialise to -1 for command not found
			if(message.equals("#")){clientCommand = -2;}// Client is asking for the list of commands
			for(int i = 0; i < COMMANDS.length; i++){
				if(clientString[0].equalsIgnoreCase(COMMANDS[i][0])){
					clientCommand = i;
				}
			}
			if(!runCommand(clientCommand,clientString) && debug){
				String errorMessage = "Command \"" + clientString[0] + 
						"\" did not run correctly! " + NEWLINE +
						"Please check your syntax!" + NEWLINE +
						"-------------------------------------------" + NEWLINE +
						COMMANDS[clientCommand][0] + ": " + COMMANDS[clientCommand][1] + 
						NEWLINE + "-------------------------------------------" ;
				collabClientUI.display(errorMessage);
			}
		} else {
			try {
				sendToServer(message);
			} catch (IOException e) {
				collabClientUI.display("Could not send message to server.");
				quit();
			}
		}
	}
	
	/**
	 * This method displays the list of valid commands in <code>collabClientUI</code>.
	 */
	public void displayCommands(){
		String message = "Valid commands are:" + NEWLINE;
		message += "-------------------------------------------" + NEWLINE;
		for (int i = 0; i < COMMANDS.length; i++){
			message += COMMANDS[i][0] + ": " + COMMANDS[i][1] + NEWLINE;
			message += "-------------------------------------------" + NEWLINE;
		}
		collabClientUI.display(message);
	}
	
	/**
	 * This method gets the program name.
	 * 
	 * @return The program name.
	 */
	public String getProgramName(){
		return PROGRAM_NAME;
	}
	
	/**
	 * This method gets the program version.
	 * 
	 * @return The program version
	 */
	public String getProgramVersion(){
		return PROGRAM_VERSION;
	}
	
	/**
	 * This private method prints a valid command.
	 * 
	 * @param int The index of the command to print.	
	 * @return A string containing a command.
	 */
	private String getCommand(int index){
		if(!(index < 0) && !(index >= COMMANDS.length)){
			return (COMMANDS[index][0]);
		}else{
			return null;
		}
	}
	
	/**
	 * This private method runs the command that was input by the user.
	 * Returns true if the command ran and false otherwise.
	 * 
	 * @param commandIndex The index of the command to run.
	 * @param commandString An array containing the command and its' parameters.
	 * @return A boolean indicating if the command successfully ran.
	 */
	private boolean runCommand(int commandIndex, String[] commandString){
		boolean ranCommand = false;
		switch (commandIndex){
		case -2:// display commands
			displayCommands();
			ranCommand = true;
			break;
		case 0: //#quit
			//terminates any server this is running
			if(getCollabServer() != null)
			{
				getCollabServer().handleMessageFromServerUI("*quit");
			}
			//log off any server this is connected
			try{
				sendToServer("#logoff");
				}
			catch (IOException e) {
				collabClientUI.display("Error logging off!");
				}
			quit(); // / Log off the client then quit;
			ranCommand = true;
			break;
		case 1: //#logoff
			collabClientUI.display("Attempting to logoff.");
			//terminates any server this is running
			if(getCollabServer() != null)
			{
				getCollabServer().handleMessageFromServerUI("*quit");
			}
			//log off client
			if(isConnected()){
				try {
					sendToServer("#logoff");
					closeConnection();
					ranCommand = true;
				} 
				catch (IOException e) {
					collabClientUI.display("Error logging off!");
				}
			}else{
				collabClientUI.display("Already logged off!");
			}
			break;
		case 2: //#sethost
			if(!isConnected()){
				try{
					setHost(commandString[1]);
					ranCommand = true; 
				}
				catch(ArrayIndexOutOfBoundsException e)
				{
					break;
				}
			}
			handleMessageFromClientUI("#gethost");// Get and print the host.
			break;
		case 3: //#setport
			if(!isConnected()){
				try{
					int result = Integer.parseInt(commandString[1]);
					if(result >= 1024 && result <= 65535){
					setPort(result);
					ranCommand = true;
					}else{
						collabClientUI.display("Invalid port number " + result + "!" + NEWLINE + 
								"The port should be between 1024 and 65535.");
					}
				}
				catch(ArrayIndexOutOfBoundsException e)
				{
					break;
				}
				catch(NumberFormatException e)
				{
					break;
				}
			}
			handleMessageFromClientUI("#getport");// Get and print the port.
			break;
		case 4: //#login
			if(!isConnected()){
				collabClientUI.display("Attempting to connect to " + getHost() + ":" + getPort() + ".");
				try {
					try{
						loginID = commandString[1];
					}
					catch(ArrayIndexOutOfBoundsException e){
						//Do Nothing - use initial loginID
					}
					handleConnectionToServer();
					collabClientUI.display("Connected to " + getHost() + ".");
					ranCommand = true;
				} catch (IOException e) {
					String message = "Error: Can't setup connection to " + getHost() + 
							":" + getPort() + "!"; 
					if(debug) 
						message +=NEWLINE + 
						"You may want to change the host using: " + getCommand(2) +
						NEWLINE + "or change the port using: " + getCommand(3);
					collabClientUI.display(message);
				}
			}
			break;
		case 5: //#gethost
			collabClientUI.display("The host is set to \"" + getHost() + "\".");
			ranCommand = true;
			break;
		case 6: //#getport
			collabClientUI.display("The port is set to " + getPort() + ".");
			ranCommand = true;
			break;
		case 7: //#debug
			String message;
			if(!debug)
			{
				debug = true;
				message = "debug is on";
			}
			else
			{
				debug = false;
				message = "debug is off";
			}
			collabClientUI.display(message);
			ranCommand = true;
			break;
		default:
			collabClientUI.display(commandString[0] + " is not a valid command!");
			if(debug)
				displayCommands();
			ranCommand = true;
			break;
		}
		return ranCommand;
	}
	
	/**
	 * This method terminates the client.
	 */
	public void quit()
	{
		try
		{
			closeConnection();
		}
		catch(IOException e) {}
		collabClientUI.display("Terminating " + PROGRAM_NAME + ".");
		delete();
		System.exit(0);
	}
	
	/**
	 * Implementation of Hook method called after the connection has been closed.
	 * This method is overriding from the superclass to
	 * inform the client using <code>collabClientUI</code> that the connection has closed.
	 */
	@Override
	protected void connectionClosed() {
		collabClientUI.display("Connection closed.");
	}
	
	/**
	 * Implementation of Hook method called each time an exception is thrown by the client's
	 * thread that is waiting for messages from the server. This method is
	 * overriding from the superclass.
	 * 
	 * @param exception
	 *            the exception raised.
	 */
	protected void connectionException(Exception exception) {
		if(debug)
			collabClientUI.display
			("The connection to the server raised \"" + exception.toString() + "\".");
		connectionClosed();
		if(debug)
			collabClientUI.display("To attempt to reconnect select \"" + getCommand(4) + "\"");
	}

	/* Code from Umple - edited*/

	//------------------------
	// INTERFACE
	//------------------------

	/**
	 * This method gets the instance of <code>CollabClientUI</code>.
	 * 
	 * @return The instance of <code>CollabClientUI</code>.
	 */
	public CollabIF getCollabClientUI()
	{
		return collabClientUI;
	}

	/**
	 * This method gets the instance of <code>CollabServer</code>.
	 * 
	 * @return The instance of <code>CollabServer</code>.
	 */
	public CollabServer getCollabServer()
	{
		return collabServer;
	}

	/**
	 * This method gets the instance of <code>CollaborationApp</code> at index <code>index</code>.
	 * 
	 * @param index The index of the <code>CollaborationApp</code> to get.
	 * @return The instance of <code>CollaborationApp</code> at index <code>index</code>.
	 */
	public CollaborationApp getCollaborationApp(int index)
	{
		CollaborationApp aCollaborationApp = null;
		if(index >= minimumNumberOfCollaborationApps() && index < maximumNumberOfCollaborationApps()){
			aCollaborationApp = collaborationApps[index];
		}
		return aCollaborationApp;
	}

	/**
	 * This method gets the number of <code>CollaborationApp</code>s.
	 * 
	 * @return The number of <code>CollaborationApp</code>s.
	 */
	public int numberOfCollaborationApps()
	{
		int number = 0;
		for(int index = 0; index < collaborationApps.length; index++){
			if(collaborationApps[index].getUniqueID() != CollaborationApp.serialVersionUID){
				number++;
			}
		}
		return number;
	}

	/**
	 * This method checks if there are any <code>CollaborationApp</code>s.
	 * 
	 * @return A <code>boolean</code> that indicates if there are any <code>CollaborationApp</code>s.
	 */
	public boolean hasCollaborationApps()
	{
		boolean has = numberOfCollaborationApps() > 0;
		return has;
	}

	/**
	 * @param newCollabClientUI
	 * @return
	 */
	public boolean setCollabClientUI(CollabClientUI newCollabClientUI)
	{
		boolean wasSet = false;
		if (collabClientUI != null && !collabClientUI.equals(newCollabClientUI) && equals(collabClientUI.getCollabClient()))
		{
			//Unable to setCollabClientUI, as existing collabClientUI would become an orphan
			return wasSet;
		}

		collabClientUI = newCollabClientUI;
		CollabClient oldCollabClient = newCollabClientUI != null ? newCollabClientUI.getCollabClient() : null;

		if (!this.equals(oldCollabClient))
		{
			if (oldCollabClient != null)
			{
				oldCollabClient.collabClientUI = null;
			}
			if (collabClientUI != null)
			{
				collabClientUI.setCollabClient(this);
			}
		}
		wasSet = true;
		return wasSet;
	}

	/**
	 * This method sets the instance of <code>CollabServer</code>.
	 * Returns true if it was set and false otherwise.
	 * 
	 * 
	 * @param newCollabServer The instance of <code>CollabServer</code> to set.
	 * @return If the instance of <code>CollabServer</code> was set.
	 */
	public boolean setCollabServer(CollabServer newCollabServer)
	{
		boolean wasSet = false;
		if (collabServer != null && !collabServer.equals(newCollabServer) && equals(collabServer.getCollabClient()))
		{
			//Unable to setCollabServer, as existing collabServer would become an orphan
			return wasSet;
		}

		collabServer = newCollabServer;
		CollabClient oldCollabClient = newCollabServer != null ? newCollabServer.getCollabClient() : null;

		if (!this.equals(oldCollabClient))
		{
			if (oldCollabClient != null)
			{
				oldCollabClient.collabServer = null;
			}
			if (collabServer != null)
			{
				collabServer.setCollabClient(this);
			}
		}
		wasSet = true;
		return wasSet;
	}

	public static int minimumNumberOfCollaborationApps()
	{
		return 0;
	}

	public static int maximumNumberOfCollaborationApps()
	{
		return 5;
	}
	
	/**
	 * This method returns an <code>int</code> indicating the index of the next available <code>CollaborationApp</code> slot.
	 * It returns a number between the minimum and maximum - 1 of  the next available <code>CollaborationApp</code> slot and 
	 * -1 if no slots are available.
	 * 
	 * @return The index of the next available <code>CollaborationApp</code> slot.
	 */
	public int indexOfNextCollaborationApp()
	{
		int number = -1;
		for(int index = 0; index < collaborationApps.length; index++){
			if(collaborationApps[index].getUniqueID() == CollaborationApp.serialVersionUID){
				number = index;
				return number;
			}
		}
		return number;
	}
	
	/**
	 * This method sets the instance of <code>CollaborationApp</code> at index <code>index</code>.
	 * Returns true if it was set and false otherwise.
	 * 
	 * @param aCollaborationApp The instance of <code>CollaborationApp</code> to set.
	 * @param index the index of the <code>CollaborationApp</code> to set.
	 * @return If the <code>CollaborationApp</code> was set.
	 */
	public boolean addCollaborationAppAt(CollaborationApp aCollaborationApp, int index)
	{
		boolean wasAdded = false;
		if (numberOfCollaborationApps() >= maximumNumberOfCollaborationApps())
		{
			return wasAdded;
		}
		else if(index >= minimumNumberOfCollaborationApps() && index < maximumNumberOfCollaborationApps())
		{
			CollaborationApp temp = collaborationApps[index];
			collaborationApps[index] = aCollaborationApp;
			temp.delete();
			wasAdded=true;
		}
		return wasAdded;
	}
	
	/**
	 This method removes the instance of <code>CollaborationApp</code> at index <code>index</code>.
	 * Returns true if it was removed and false otherwise.
	 * 
	 * @param index the index of the <code>CollaborationApp</code> to remove.
	 * @return If the <code>CollaborationApp</code> was removed.
	 */
	public boolean removeCollaborationAppAt(int index)
	{
		boolean wasRemoved = false;
		if(index >= minimumNumberOfCollaborationApps() && index < maximumNumberOfCollaborationApps()){
			CollaborationApp temp;
			boolean nullPointer = false;
			try{
				temp = collaborationApps[index];
			}catch (Exception e){
				nullPointer = true;
				temp = null;
				return wasRemoved;
			}
			collaborationApps[index] = new CollaborationApp(this,index);
			if(!nullPointer)
				temp.delete();
			wasRemoved=true;
		}
		return wasRemoved;
	}

	
	/**
	 * This method deletes all the elements used by this instance of <code>CollabClient</code>.
	 */
	public void delete()
	{
		CollabIF existingCollabClientUI = collabClientUI;
		collabClientUI = null;
		if (existingCollabClientUI != null)
		{
			existingCollabClientUI.delete();
		}
		CollabServer existingCollabServer = collabServer;
		collabServer = null;
		if (existingCollabServer != null)
		{
			existingCollabServer.delete();
		}
		for(int i= maximumNumberOfCollaborationApps(); i > 0; i--)
		{
			CollaborationApp aCollaborationApp = collaborationApps[i - 1];
			aCollaborationApp.delete();
		}
	}
}
